#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Nuitka打包脚本
根据项目需求配置不同目录的打包策略
"""

import os
import sys
import shutil
import subprocess
from pathlib import Path

# 项目根目录
PROJECT_ROOT = Path(__file__).parent.absolute()

def _is_ascii(s: str) -> bool:
    try:
        s.encode("ascii")
        return True
    except Exception:
        return False

DEFAULT_BUILD_DIR = PROJECT_ROOT / "build"
FALLBACK_BUILD_DIR = Path("C:/_mt_build").absolute()
BUILD_DIR = DEFAULT_BUILD_DIR
if not _is_ascii(str(BUILD_DIR)):
    BUILD_DIR = FALLBACK_BUILD_DIR

def _get_short_path(p: str) -> str:
    """获取Windows短路径，减少长路径导致的构建异常"""
    try:
        import ctypes
        buf = ctypes.create_unicode_buffer(260)
        r = ctypes.windll.kernel32.GetShortPathNameW(p, buf, len(buf))
        if r > 0:
            return buf.value
    except Exception:
        pass
    return p

def _detect_compiler_flag() -> str:
    """检测可用编译器，优先选择MSVC，其次MinGW64，再次clang（clang在缺少VS链接器时易失败）"""
    try:
        import shutil as _sh
        # 路径含非ASCII时优先选择 MinGW64，避免 MSVC 链接阶段路径问题
        if not _is_ascii(str(PROJECT_ROOT)) or not _is_ascii(str(BUILD_DIR)):
            gcc = _sh.which("gcc")
            gxx = _sh.which("g++")
            if gcc or gxx:
                return "--mingw64"
        if _sh.which("cl.exe"):
            return "--msvc=14.3"
        # 优先选择 MinGW64（conda 提供的 gcc/g++ 链路健壮）
        gcc = _sh.which("gcc")
        gxx = _sh.which("g++")
        if gcc or gxx:
            return "--mingw64"
        if _sh.which("clang-cl"):
            return "--clang"
    except Exception:
        pass
    return "--msvc=14.2"

def _opengl_nofollow_args() -> list:
    args = []
    try:
        # 2025-11: 不再使用 nofollow，改为直接复制包源码，避免缺包
        pass
    except Exception:
        pass
    return args

def _opengl_data_args() -> list:
    args: list = []
    try:
        import OpenGL
        ogl_dir = Path(OpenGL.__file__).parent
        # 复制整个 OpenGL 包到 dist，确保运行时可导入
        args.append(f"--include-data-dir={_get_short_path(str(ogl_dir))}=OpenGL")
    except Exception:
        pass
    return args

def _numpy_nofollow_args() -> list:
    args = []
    try:
        args.extend([
            "--nofollow-import-to=numpy",
            "--noinclude-custom-mode=numpy:nofollow",
            "--nofollow-import-to=numpy.compat",
            "--noinclude-custom-mode=numpy.compat:nofollow",
        ])
    except Exception:
        pass
    return args

def _numpy_data_args() -> list:
    args: list = []
    try:
        import numpy
        np_dir = Path(numpy.__file__).parent
        core_dir = np_dir / "core"
        alt_core_dir = np_dir / "_core"
        libs_dir = np_dir / "libs"
        def add_glob(src_dir: Path, pattern: str, dest_rel: str):
            try:
                from glob import glob
                files = glob(str(src_dir / pattern))
                if files:
                    args.append(f"--include-data-files={_get_short_path(str(src_dir))}\\{pattern}={dest_rel}")
            except Exception:
                pass
        if core_dir.exists():
            add_glob(core_dir, "*.pyd", "numpy/core/")
            add_glob(core_dir, "*.dll", "numpy/core/")
        if alt_core_dir.exists():
            add_glob(alt_core_dir, "*.pyd", "numpy/_core/")
            add_glob(alt_core_dir, "*.dll", "numpy/_core/")
        if libs_dir.exists():
            add_glob(libs_dir, "*.dll", "numpy/libs/")
        try:
            env_root = np_dir.parents[2]
            bin_dir = env_root / "Library" / "bin"
            if bin_dir.exists():
                patterns = ["mkl*.dll", "libiomp*.dll", "openblas*.dll", "lapack*.dll", "blas*.dll"]
                from glob import glob
                for pat in patterns:
                    if glob(str(bin_dir / pat)):
                        args.append(f"--include-data-files={_get_short_path(str(bin_dir))}\\{pat}=./")
        except Exception:
            pass
    except Exception:
        pass
    return args

def _numpy_top_files_args() -> list:
    args: list = []
    try:
        import numpy
        np_dir = Path(numpy.__file__).parent
        def add_file(src: Path, dest_rel: str):
            if src.exists():
                args.append(f"--include-data-file={_get_short_path(str(src))}={dest_rel}")
        add_file(np_dir / "__init__.py", "numpy/__init__.py")
        add_file(np_dir / "_distributor_init.py", "numpy/_distributor_init.py")
        add_file(np_dir / "__config__.py", "numpy/__config__.py")
        add_file(np_dir / "version.py", "numpy/version.py")
        add_file(np_dir / "_globals.py", "numpy/_globals.py")
    except Exception:
        pass
    return args

def _pyqtgraph_data_args() -> list:
    args: list = []
    try:
        import pyqtgraph
        pg_dir = Path(pyqtgraph.__file__).parent
        opengl_dir = pg_dir / "opengl"
        if opengl_dir.exists():
            has_data = False
            try:
                for root, dirs, files in os.walk(str(opengl_dir)):
                    for name in files:
                        ext = Path(name).suffix.lower()
                        if ext not in (".py", ".pyc", ".pyo"):
                            has_data = True
                            break
                    if has_data:
                        break
            except Exception:
                has_data = False
            if has_data:
                args.append(f"--include-data-dir={_get_short_path(str(opengl_dir))}=pyqtgraph/opengl")
                print(f"Info: 找到并添加 pyqtgraph opengl 数据目录: {opengl_dir}")
            else:
                print(f"Info: pyqtgraph/opengl 目录无额外数据文件，跳过 include-data-dir")
    except Exception as e:
        print(f"警告: 未能自动检查 pyqtgraph opengl 目录: {e}")
    return args

def _qt_opengl_runtime_files():
    files = []
    try:
        mode = os.environ.get("MOOETOOL_GL_MODE", "desktop").lower()
        import PySide2
        base = Path(PySide2.__file__).resolve().parent / "Qt" / "bin"
        libbin = Path(sys.prefix) / "Library" / "bin"
        candidates = []
        if mode == "angle":
            candidates += [
                base / "libEGL.dll",
                base / "libGLESv2.dll",
                base / "d3dcompiler_47.dll",
                libbin / "libEGL.dll",
                libbin / "libGLESv2.dll",
                libbin / "d3dcompiler_47.dll",
            ]
        elif mode in ("mesa", "software"):
            candidates += [
                base / "opengl32sw.dll",
                libbin / "opengl32sw.dll",
                libbin / "osmesa.dll",
                libbin / "glapi.dll",
            ]
        else:
            candidates += [
                base / "d3dcompiler_47.dll",
                libbin / "d3dcompiler_47.dll",
            ]
        try:
            sysroot = Path(os.environ.get("SystemRoot", "C:/Windows"))
            candidates += [
                sysroot / "System32" / "d3dcompiler_47.dll",
                sysroot / "SysWOW64" / "d3dcompiler_47.dll",
            ]
        except Exception:
            pass
        seen = set()
        for p in candidates:
            try:
                rp = p.resolve()
            except Exception:
                rp = p
            if rp.exists():
                sp = str(rp)
                if sp not in seen:
                    files.append(sp)
                    seen.add(sp)
    except Exception:
        pass
    return files

def clean_build_dir():
    """清理构建目录"""
    if BUILD_DIR.exists():
        print(f"清理构建目录: {BUILD_DIR}")
        shutil.rmtree(BUILD_DIR)
    BUILD_DIR.mkdir(exist_ok=True)

def copy_directory(src, dst, exclude_content=False):
    """复制目录
    
    Args:
        src: 源目录
        dst: 目标目录
        exclude_content: 如果为True，只创建目录结构，不复制内容
    """
    src_path = Path(src)
    dst_path = Path(dst)
    
    if not src_path.exists():
        print(f"警告: 源目录不存在 {src_path}")
        return
    
    if exclude_content:
        # 只创建目录，不复制内容
        dst_path.mkdir(parents=True, exist_ok=True)
        print(f"创建空目录: {dst_path}")
    else:
        # 复制整个目录
        if dst_path.exists():
            shutil.rmtree(dst_path)
        shutil.copytree(src_path, dst_path)
        print(f"复制目录: {src_path} -> {dst_path}")

def copy_msg_files():
    """复制ros1/msg目录和vehicle目录到apis模块中"""
    # 复制msg目录
    src_msg = PROJECT_ROOT / "apis" / "ros1" / "msg"
    dst_msg = BUILD_DIR /"main.dist"/ "apis" / "ros1" / "msg"
    
    if src_msg.exists():
        dst_msg.parent.mkdir(parents=True, exist_ok=True)
        copy_directory(src_msg, dst_msg)
        print(f"复制msg目录: {src_msg} -> {dst_msg}")
    else:
        print(f"警告: msg目录不存在 {src_msg}")
    
    # 复制vehicle目录
    src_vehicle = PROJECT_ROOT / "apis" / "ros1" / "vehicle"
    dst_vehicle = BUILD_DIR /"main.dist"/ "apis" / "ros1" / "vehicle"
    
    if src_vehicle.exists():
        dst_vehicle.parent.mkdir(parents=True, exist_ok=True)
        copy_directory(src_vehicle, dst_vehicle)
        print(f"复制vehicle目录: {src_vehicle} -> {dst_vehicle}")
    else:
        print(f"警告: vehicle目录不存在 {src_vehicle}")

def copy_resource_app_files():
    """复制resource/app目录到main.dist/resource/"""
    src_app = PROJECT_ROOT / "resource" / "app"
    dst_app = BUILD_DIR / "main.dist" / "resource" / "app"
    
    if src_app.exists():
        dst_app.parent.mkdir(parents=True, exist_ok=True)
        copy_directory(src_app, dst_app)
        print(f"复制resource/app目录: {src_app} -> {dst_app}")
    else:
        print(f"警告: resource/app目录不存在 {src_app}")

def build_with_nuitka():
    """使用Nuitka进行打包"""
    print("开始Nuitka打包...")
    build_dir_short = _get_short_path(str(BUILD_DIR))
    project_root_short = _get_short_path(str(PROJECT_ROOT))
    scons_temp_dir = _get_short_path(str(Path("C:/_mt_scons").absolute()))
    try:
        Path(scons_temp_dir).mkdir(parents=True, exist_ok=True)
    except Exception:
        pass
    compiler_flag = _detect_compiler_flag()
    
    # Nuitka命令参数
    nuitka_args = [
        sys.executable, "-m", "nuitka",
        "--standalone",
        # "--windows-console-mode=disable",
        compiler_flag,
        "--lto=no",
        "--jobs=4",
        "--show-memory",
        "--show-progress",
        "--windows-uac-admin",
        "--enable-plugin=pyside2",
        "--enable-plugin=numpy",
        "--include-qt-plugins=platforms,imageformats,styles,iconengines,printsupport,platforminputcontexts",
        "--include-package=rosbags",
        "--include-package=pyqtgraph",
        "--include-package=pyqtgraph.opengl",
        # 保留 OpenGL 作为包含包，同时复制源码，避免遗漏
        "--include-package=OpenGL",
        "--include-package=numpy",
        "--include-package=websockets",
        "--follow-import-to=apis,gglobal,mooetoolapp",
        "--include-data-dir=resource=resource",
        "--include-data-dir=config=config",
        "--windows-icon-from-ico=resource/qrc/bitbug_favicon.ico",
        f"--output-dir={build_dir_short}",
        "--output-filename=MooeTool.exe",
        "main.py"
    ]

    # 显式复制 OpenGL 源码目录，保证导入可用
    nuitka_args.extend(_opengl_data_args())
    
    # 添加 PyOpenGL 官方插件
    try:
        import OpenGL
        opengl_base_path = Path(OpenGL.__file__).parent
        # PyOpenGL的Nuitka插件位于 "OpenGL/plugins/nuitka.py"
        opengl_plugin_file = opengl_base_path / "plugins" / "nuitka.py"
        if opengl_plugin_file.exists():
            nuitka_args.append(f"--user-plugin={str(opengl_plugin_file)}")
        else:
            print(f"警告: 未找到 PyOpenGL Nuitka 插件: {opengl_plugin_file}")
    except Exception as e:
        print(f"警告: 未能自动添加 PyOpenGL Nuitka 插件: {e}")

    # 显式包含NumPy运行时文件，提高兼容性
    nuitka_args.extend(_numpy_data_args())
    # 显式包含NumPy顶层Python文件，确保提供numpy.ndarray等符号
    nuitka_args.extend(_numpy_top_files_args())

    # 显式包含 pyqtgraph 的 shaders
    nuitka_args.extend(_pyqtgraph_data_args())

    # 可选包含 VisPy（仅当环境中安装时）
    def _vispy_data_args() -> list:
        args_local: list = []
        try:
            import vispy
            base = Path(vispy.__file__).parent
            glsl_dir = base / "glsl"
            if glsl_dir.exists():
                args_local.append(f"--include-data-dir={_get_short_path(str(glsl_dir))}=vispy/glsl")
                print(f"Info: 添加 VisPy GLSL 目录: {glsl_dir}")
        except Exception as e:
            print(f"Info: 跳过 VisPy GLSL 资源: {e}")
        return args_local

    try:
        import vispy  # noqa: F401
        nuitka_args.extend([
            "--include-package=vispy",
            "--include-package=vispy.scene",
            "--include-package=vispy.visuals",
            "--include-package=vispy.app",
            "--include-package=vispy.app.backends",
            #新增
            "--include-package=vispy.app.backends._qt",
            "--include-package=vispy.scene.cameras",
            "--include-package=vispy.scene.cameras.turntable",
            "--include-package=vispy.util",
            "--include-package=vispy.util.event",
            "--include-package=vispy.glsl",
            # "--include-package=vispy.glsl.shaders",
            "--include-package=vispy.ext",
        ])
        nuitka_args.extend(_vispy_data_args())
        nuitka_args.extend([
            "--include-module=vispy.app.backends._qt",
        ])
    except Exception as e:
        print(f"Info: 未安装 VisPy，跳过相关模块打包: {e}")

    for f in _qt_opengl_runtime_files():
        nuitka_args.append(f"--include-data-files={f}=./")
    
    try:
        # 执行Nuitka命令，直接输出到命令行
        print(f"执行命令: {' '.join(nuitka_args)}")
        env = os.environ.copy()
        env["CLCACHE_DISABLE"] = "1"
        env["NUITKA_DISABLE_CCACHE"] = "1"
        env["NUITKA_SCONS_TEMP"] = scons_temp_dir
        env["TMP"] = scons_temp_dir
        env["TEMP"] = scons_temp_dir
        if compiler_flag == "--mingw64":
            mingw_bin = Path(sys.prefix) / "Library" / "mingw-w64" / "bin"
            if mingw_bin.exists():
                env["PATH"] = str(mingw_bin) + os.pathsep + env.get("PATH", "")
            env["CC"] = "gcc"
            env["CXX"] = "g++"
            env["LD"] = "g++"
            for k in ("VSINSTALLDIR","VCINSTALLDIR","VisualStudioVersion","CL","LINK","LIB","INCLUDE"):
                env.pop(k, None)
        result = subprocess.run(nuitka_args, cwd=project_root_short, check=True, env=env)
        print("Nuitka打包成功!")
    except subprocess.CalledProcessError as e:
        print(f"Nuitka打包失败: {e}")
        return False
    except FileNotFoundError:
        print("错误: 未找到nuitka，请先安装: pip install nuitka")
        return False
    
    return True

def post_build_setup():
    """打包后的设置"""
    print("执行打包后设置...")
    
    # 1. 创建空的db目录（不复制内容）
    db_dst = BUILD_DIR /"main.dist"/ "db"
    db_dst.mkdir(exist_ok=True)
    print(f"创建空db目录: {db_dst}")
    
    # 2. 创建空的log目录（不复制内容）
    log_dst = BUILD_DIR /"main.dist"/ "log"
    log_dst.mkdir(exist_ok=True)
    print(f"创建空log目录: {log_dst}")

    pcd_dst = BUILD_DIR / "main.dist" / "resource" / "pcd_download"
    pcd_dst.mkdir(parents=True, exist_ok=True)
    print(f"创建空pcd_download目录: {pcd_dst}")
    
    # 3. 复制msg文件到apis模块
    copy_msg_files()
    
    # 4. 复制resource/app文件夹到main.dist/resource/
    copy_resource_app_files()
    
    print("打包后设置完成!")

def main():
    """主函数"""
    print("=== MooeTool Nuitka 打包脚本 ===")
    print(f"项目根目录: {PROJECT_ROOT}")
    print(f"构建目录: {BUILD_DIR}")
    
    try:
        # 1. 清理构建目录
        clean_build_dir()
        
        # 2. 使用Nuitka打包
        if not build_with_nuitka():
            print("打包失败，退出")
            return False
        
        # 3. 执行打包后设置
        post_build_setup()
        
        print("\n=== 打包完成 ===")
        print(f"输出目录: {BUILD_DIR}")
        print("\n打包策略总结:")
        print("1. ✓ apis - 作为整体模块打包，msg目录已复制")
        print("2. ✓ config - 作为数据文件打包")
        print("3. ✓ db - 只创建空目录")
        print("4. ✓ docs - 不处理")
        print("5. ✓ gglobal - 作为整体模块打包")
        print("6. ✓ log - 只创建空目录")
        print("7. ✓ mooetoolapp - 作为整体模块打包")
        print("8. ✓ resource - 作为数据文件打包，app目录已单独复制，pcd_download目录已创建")
        print("9. ✓ testcase - 不处理")
        print("10. ✓ main.py - 主程序入口")
        print("11. ✓ OpenGL 与 pyqtgraph.opengl 依赖已包含，Qt OpenGL 运行库已打包")
        
        return True
        
    except Exception as e:
        print(f"打包过程中发生错误: {e}")
        import traceback
        traceback.print_exc()
        return False

if __name__ == "__main__":
    success = main()
    sys.exit(0 if success else 1)
